#include "halley/tools/project/project_loader.h"
#include "halley/game/halley_statics.h"
#include "halley/tools/file/filesystem.h"
#include "halley/support/debug.h"
#include "halley/support/logger.h"
#include "halley/utils/algorithm.h"
using namespace Halley;


ProjectLoader::ProjectLoader(const HalleyStatics& statics, Path halleyPath, Vector<String> disabledPlatforms)
	: statics(statics)
	, halleyPath(std::move(halleyPath))
	, disabledPlatforms(std::move(disabledPlatforms))
{
	loadPlugins();
}

void ProjectLoader::setDisabledPlatforms(Vector<String> platforms)
{
	disabledPlatforms = std::move(platforms);
}

void ProjectLoader::setImporterOption(std::string_view key, ConfigNode value)
{
	importerOptions.ensureType(ConfigNodeType::Map);
	importerOptions[key] = std::move(value);
}

std::unique_ptr<Project> ProjectLoader::loadProject(const Path& path) const
{
	auto proj = std::make_unique<Project>(path, halleyPath, disabledPlatforms);
	setupPlugins(*proj);	
	return proj;
}

void ProjectLoader::setupPlugins(Project& project) const
{
	auto platforms = project.getPlatforms();

	std_ex::erase_if(platforms, [&] (const String& platform)
	{
		return disabledPlatforms.contains(platform) || (platform != "pc" && !knownPlatforms.contains(platform));
	});

	project.setPlatforms(platforms);
	project.setupImporter(getPlugins(platforms), importerOptions);
}

const Vector<String>& ProjectLoader::getKnownPlatforms() const
{
	return knownPlatforms;
}

static String getDLLExtension()
{
#if defined (_WIN32)
	return ".dll";
#elif defined(__APPLE__)
	return ".dylib";
#else
	return ".so";
#endif	
}

void ProjectLoader::loadPlugins()
{
	knownPlatforms.clear();
	//knownPlatforms.push_back("pc");

	// Look for plugins
	auto pluginPath = halleyPath / "plugins";
	auto files = FileSystem::enumerateDirectory(pluginPath);
	for (auto& file: files) {
		if (file.getExtension() == getDLLExtension()) {
			auto plugin = loadPlugin(pluginPath / file);

			// Don't mix debug and release plugin/editor
			if (plugin && plugin->isDebug() == Debug::isDebug()) {
				for (auto& plat: plugin->getSupportedPlatforms()) {
					if (plat != "*" && !knownPlatforms.contains(plat)) {
						knownPlatforms.push_back(plat);
					}
				}
				plugins.push_back(std::move(plugin));
				Logger::logInfo("With plugin: " + pluginPath / file);
			}
		}
	}
}

Vector<HalleyPluginPtr> ProjectLoader::getPlugins(Vector<String> platforms) const
{
	Vector<HalleyPluginPtr> result;

	// Initialize known platforms
	Vector<bool> foundPlatforms(platforms.size());
	for (size_t i = 0; i < platforms.size(); ++i) {
		foundPlatforms[i] = platforms[i] == "pc";
	}

	for (auto& plugin: plugins) {
		bool accepted = false;
		for (size_t i = 0; i < platforms.size(); ++i) {
			for (auto& pluginPlatform: plugin->getSupportedPlatforms()) {
				accepted |= pluginPlatform == "*" || pluginPlatform == platforms[i];
				foundPlatforms[i] = foundPlatforms[i] || pluginPlatform == platforms[i];
			}
		}
		if (accepted) {
			result.emplace_back(plugin);
		}
	}
	
	for (size_t i = 0; i < platforms.size(); ++i) {
		if (!foundPlatforms[i]) {
			Logger::logError("Unknown platform: \"" + platforms[i] + "\".");
		}
	}

	return result;
}

#ifdef _WIN32

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

namespace {
	HMODULE dlopen(const char* filename, int) { return LoadLibrary(filename); }
	bool dlclose(HMODULE handle) { return FreeLibrary(handle); }
	FARPROC dlsym(HMODULE handle, const char* symbolName) { return GetProcAddress(handle, symbolName); }

	int RTLD_LAZY = 1;
}

#else

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

#endif

HalleyPluginPtr ProjectLoader::loadPlugin(const Path& path) const
{
	using CreateHalleyPluginFn = IHalleyPlugin*(HalleyStatics*);
	using DestroyHalleyPluginFn = void(IHalleyPlugin*);
	using GetHalleyPluginVersionFn = int();

	auto module = dlopen(path.getNativeString().c_str(), RTLD_LAZY);
	if (!module) {
		return {};
	}

	auto getHalleyPluginVersion = reinterpret_cast<GetHalleyPluginVersionFn*>(dlsym(module, "getHalleyPluginVersion"));
	if (!getHalleyPluginVersion || getHalleyPluginVersion() != 2) {
		dlclose(module);
		return {};
	}

	auto createHalleyPlugin = reinterpret_cast<CreateHalleyPluginFn*>(dlsym(module, "createHalleyPlugin"));
	auto destroyHalleyPlugin = reinterpret_cast<DestroyHalleyPluginFn*>(dlsym(module, "destroyHalleyPlugin"));
	if (!createHalleyPlugin || !destroyHalleyPlugin) {
		dlclose(module);
		return {};
	}

	return HalleyPluginPtr(createHalleyPlugin(const_cast<HalleyStatics*>(&statics)), [=] (IHalleyPlugin* plugin)
	{
		destroyHalleyPlugin(plugin);
		dlclose(module);
	});
}
